# busboy模块
30 秒速记
busboy是专门解析multipart/form-data的流式解析器,边收边吐,文件不会整块进内存- 它只管「拆」:文本字段走
field事件,文件走file事件并交给你一个可读流,存哪由你决定 - 每个文件流必须被消费(
pipe出去或resume()),否则整个解析卡住,结束事件永远不来 - 默认没有大小和数量上限,
limits里的fileSize、files、fields要自己配 - 版本差异:
0.x是new Busboy()+finish;1.x改成busboy()函数 +close,file回调参数也变成(name, stream, info)
busboy 是一个流式的 multipart 解析器,它把上传请求拆成「字段」和「文件流」两类事件交给你处理。 浏览器用表单传文件时,请求体是一段用 boundary 分隔的二进制数据,busboy 一边读请求流一边切,所以传一个 1GB 的视频,进程内存也只占缓冲区那点。它的定位很底层,不帮你落盘、不帮你限流、不帮你改名,multer、koa-body 这类上层中间件内部用的就是它或者同类库。用它最容易踩的坑是不想要某个文件就直接 return,那个流没人读,解析会一直挂着。
理解「busboy模块」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
busboy 在 2022 年发布 1.0 时改了 API,网上大量教程还是 0.x 写法,两者放一起对照最清楚。
// busboy 0.x
const Busboy = require('busboy')
const bb = new Busboy({ headers: req.headers })
bb.on('file', (fieldname, file, filename, encoding, mimetype) => {
file.pipe(fs.createWriteStream(`/tmp/${Date.now()}`))
})
bb.on('field', (fieldname, val, fieldnameTruncated, valTruncated) => {})
bb.on('finish', () => res.end('ok'))
req.pipe(bb)
// busboy 1.x
const busboy = require('busboy')
const { randomUUID } = require('crypto')
const bb = busboy({
headers: req.headers,
limits: { fileSize: 5 * 1024 * 1024, files: 1, fields: 10 },
})
bb.on('file', (name, file, info) => {
const { filename, mimeType } = info // 第三个参数是对象
if (!mimeType.startsWith('image/')) return file.resume() // 不要的也得读掉
file.on('limit', () => console.log('超过 5MB,被截断'))
file.pipe(fs.createWriteStream(`/tmp/${randomUUID()}`))
})
bb.on('field', (name, val, info) => {
// info.nameTruncated / info.valueTruncated 表示是否被 limits 截断
})
bb.on('close', () => res.end('ok')) // 结束事件叫 close
req.pipe(bb)
记住三处变化就够了:构造方式从 new Busboy() 变成调用 busboy();file / field 回调把元信息收进 info 对象;结束事件从 finish 换成 close。另外 1.x 遇到缺失或不支持的 Content-Type 会在构造时直接抛错,外面要包 try/catch 返回 415。
面试官追问
追问 1Koa 里已经用了 koa-bodyparser,为什么 ctx.request.body 拿不到上传的文件?
koa-bodyparser 只处理 json、form、text 三种类型,multipart/form-data 它直接跳过。文件上传得换 busboy、koa-body(开 multipart: true)或者 @koa/multer 这类专门的解析器。
追问 2用户传一个 2GB 的文件,busboy 会不会把内存撑爆?
正常不会,它是流式的,file.pipe(fs.createWriteStream(path)) 自带背压,磁盘写慢了就暂停读。会出事的是你在 data 事件里把 chunk 全 push 进数组再 Buffer.concat,那就又退回整块进内存了。
追问 3表单里有三个文件,业务只要第一个,后面两个不处理会怎样?
请求会卡住。busboy 要把每个文件流读完才能继续往后解析,没人读的流把整个管道堵死,结束事件不触发,接口一直 pending。不要的文件调一句 file.resume() 把数据丢掉就行。
追问 4直接用 busboy 和用 multer / koa-body,怎么选?
一般业务我直接用上层中间件,限制、临时目录、错误处理都包好了。只有要把文件流直接转发到对象存储、边传边算哈希这类需要精确控制流的场景,才值得自己写 busboy。
追问 5照着老教程写的 new Busboy({ headers }),升级到 1.x 后报 filename.split is not a function,为什么?
1.x 的 file 事件签名变成了 (name, stream, info),第三个参数是 { filename, encoding, mimeType } 对象,不再是字符串。另外结束事件改用 close。要么锁回 0.3.1,要么按新签名改代码。
# busboy模块
# 快速开始
30 秒速记
- 最小链路三步:用请求头建解析器 → 挂
file/field/ 结束事件 →req.pipe(bb) - 在
Koa里喂的是原生ctx.req,不是ctx.request - 结束事件触发后再响应,响应前别忘了文件写流也要写完
- 本地测试不用写页面:
curl -F "avatar=@a.png" -F "name=tom" localhost:3000/upload Content-Type不是multipart时1.x会在构造时抛错,要捕获后回415
快速跑通 busboy 只需要三步:拿请求头创建解析器、监听事件、把请求流 pipe 进去。 它本身就是一个可写流,所以 req.pipe(bb) 之后数据才开始流动,事件才会陆续触发。在 Koa 里要注意拿的是 ctx.req 这个 Node 原生请求对象。调试阶段我一般不写前端页面,直接用 curl -F 发 multipart 请求,字段和文件都能带,比点页面快得多。跑通之后第一件事是补 limits,默认是不限大小的。
理解「快速开始」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。
工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。
面试官追问
追问 1用 curl -d 'a=1' 测这个接口,服务直接崩了,为什么?
-d 发的是 application/x-www-form-urlencoded,这个 busboy 其实也能解析;真正会崩的是不带 Content-Type 或者发 application/json,1.x 构造时直接抛 Unsupported content type。把 busboy({ headers }) 包进 try/catch,抛错就回 415。
追问 2前端表单里文本字段放在文件后面,后端在 file 事件里读不到 userId,正常吗?
正常。multipart 是按表单顺序一段段流过来的,解析到文件时后面的字段还没到。要么前端把 userId 这类字段放在文件前面,要么等结束事件再统一用字段做业务处理。
追问 3Koa 里写成 ctx.request.pipe(bb) 为什么报错?
ctx.request 是 Koa 包装出来的对象,不是流,没有 pipe 方法。原生的 IncomingMessage 在 ctx.req 上,写 ctx.req.pipe(bb)。
追问 4在 Koa 的 async 中间件里用 busboy,为什么要包一个 Promise?
busboy
